#ifndef _MOE_LAUNCHER
#define _MOE_LAUNCHER

#include "engine/MOEA.hh"
#include "engine/Properties.hh"
#include "algorithms/NSGAII.hh"
#include "engine/StatisticsSink.hh"
#include "engine/Statistics.hh"
#include "engine/Historic.hh"
#include "engine/netstream.hh"
#include "problems/PerturbMutation.hh"
#include "problems/RouletteWheelSelection.hh"
#include "problems/TournamentSelection.hh"
#include "problems/RealSinglePointCrossover.hh"
#include "problems/SinglePointCrossover.hh"
#include "problems/DualPointCrossover.hh"
#include "problems/PolinomialMutation.hh"
#include "problems/BLXCrossover.hh"
#include "problems/SBXCrossover.hh"
#include "problems/RingMigration.hh"
#include "problems/ZDT.hh"
#include "problems/RealArraySolution.hh"
#include "OctaveExport.hh"


#include <iostream>
#include <string>
#include <sys/time.h>

using namespace std;

Solution* createSolution(Properties *properties) {
    char *solutionType = properties->get("solution.type", "realarray");

    Solution *solution = NULL;

    if (strcmp(solutionType, "realarray") == 0) {

        RealArraySolution *realSolution = new RealArraySolution();
        realSolution->initialize();
        solution = realSolution;

    } else if (strcmp(solutionType, "intarray") == 0) {

        IntArraySolution *intSolution = new IntArraySolution();
        intSolution->initialize();
        solution = intSolution;

    } else {
        cout << "solution type " << solutionType << " unknown, aborting..." << endl;
        exit(1);
    }

    return solution;
}

Population *createInitialPopulation(int populationSize, Properties *properties) {

    ArrayList<Solution> *solutions = new ArrayList<Solution>(true);

    for (int i = 0; i < populationSize; i++) {
        solutions->add(createSolution(properties));
    }

    return new Population(solutions);
}

ArrayList<Mutation> *createMutations(Properties *properties) {

    char *type = properties->get("mutation.type", "polinomial");
    double probability = atof(properties->get("mutation.probability", "0.01"));

    ArrayList<Mutation> *mutations = new ArrayList<Mutation>(true);

    if (strcmp(type, "polinomial") == 0) {

        double index = atof(properties->get("polinomial.index", "20.0"));
        mutations->add(new PolinomialMutation(index, probability));

    } else if (strcmp(type, "perturb") == 0) {

        mutations->add(new PerturbMutation(probability));
    }

    return mutations;
}

ArrayList<Restriction> *createRestrictions(Properties *properties) {
    ArrayList<Restriction> *restrictions = new ArrayList<Restriction>(true);

    char *problem = properties->get("problem", "ZDT1");

    if (strcmp(problem, "ZDT1") == 0 || strcmp(problem, "ZDT2") == 0 ||
            strcmp(problem, "ZDT3") == 0 || strcmp(problem, "ZDT6") == 0 ||
            strcmp(problem, "ZDT4") == 0) {

        double *lowerBounds = new double[RealArraySolution::getSize()];
        double *higherBounds = new double[RealArraySolution::getSize()];

        for (int i = 0; i < RealArraySolution::getSize(); i++) {
            if (strcmp(problem, "ZDT4") == 0) {
                if (i == 0) {

                    lowerBounds[i] = 0.0;
                    higherBounds[i] = 1.0;

                } else {

                    lowerBounds[i] = -5.0;
                    higherBounds[i] = 5.0;

                }

            } else {
                lowerBounds[i] = 0.0;
                higherBounds[i] = 1.0;
            }
        }

        RealArraySolution::setLowerBounds(lowerBounds);
        RealArraySolution::setHigherBounds(higherBounds);

        Restriction *restriction = new RealRestriction;
        restrictions->add(restriction);

    } else if (strcmp(problem, "ZDT5") != 0){

        cout << "problem unknown, aborting " << endl;
        exit(1);
    }

    return restrictions;
}

ArrayList<Objective> *createObjectives(Properties *properties) {

    ArrayList<Objective> *objectivesArrayList = new ArrayList<Objective>(true);

    char *problem = properties->get("problem", "ZDT1");
    Objective *objective1 = NULL;
    Objective *objective2 = NULL;

    if (strcmp(problem, "ZDT1") == 0) {

        objective1 = new Objective(ZDT1::OBJECTIVE_DIRECTION_1, ZDT1::objectiveFunction1);
        objective2 = new Objective(ZDT1::OBJECTIVE_DIRECTION_2, ZDT1::objectiveFunction2);

    } else if (strcmp(problem, "ZDT2") == 0) {

        objective1 = new Objective(ZDT2::OBJECTIVE_DIRECTION_1, ZDT2::objectiveFunction1);
        objective2 = new Objective(ZDT2::OBJECTIVE_DIRECTION_2, ZDT2::objectiveFunction2);

    } else if (strcmp(problem, "ZDT3") == 0) {

        objective1 = new Objective(ZDT3::OBJECTIVE_DIRECTION_1, ZDT3::objectiveFunction1);
        objective2 = new Objective(ZDT3::OBJECTIVE_DIRECTION_2, ZDT3::objectiveFunction2);

    } else if (strcmp(problem, "ZDT4") == 0) {

        objective1 = new Objective(ZDT4::OBJECTIVE_DIRECTION_1, ZDT4::objectiveFunction1);
        objective2 = new Objective(ZDT4::OBJECTIVE_DIRECTION_2, ZDT4::objectiveFunction2);

    } else if (strcmp(problem, "ZDT5") == 0) {

        objective1 = new Objective(ZDT5::OBJECTIVE_DIRECTION_1, ZDT5::objectiveFunction1);
        objective2 = new Objective(ZDT5::OBJECTIVE_DIRECTION_2, ZDT5::objectiveFunction2);

    } else if (strcmp(problem, "ZDT6") == 0) {

        objective1 = new Objective(ZDT6::OBJECTIVE_DIRECTION_1, ZDT6::objectiveFunction1);
        objective2 = new Objective(ZDT6::OBJECTIVE_DIRECTION_2, ZDT6::objectiveFunction2);

    } else {

        cout << "problem unknown, aborting " << endl;
        exit(1);
    }


    objectivesArrayList->add(objective1);
    objectivesArrayList->add(objective2);

    return objectivesArrayList;
}

ArrayList<Selection> *createSelections() {

    ArrayList<Selection> *selections = new ArrayList<Selection>(true);

    selections->add(new TournamentSelection());

    return selections;
}

ArrayList<Crossover> *createCrossovers(Properties *properties) {

    ArrayList<Crossover> *crossovers = new ArrayList<Crossover>(true);

    char *crossoverOperator = properties->get("crossover.type", "single.point");
    if (strcmp(crossoverOperator, "single.point") == 0) {

        crossovers->add(new SinglePointCrossover);
    } else if (strcmp(crossoverOperator, "dual.point") == 0) {

        crossovers->add(new DualPointCrossover);

    } else if (strcmp(crossoverOperator, "blx") == 0) {

        double alpha = atof(properties->get("blx.alpha", "0.5"));
        crossovers->add(new BLXCrossover(alpha));

    } else if (strcmp(crossoverOperator, "sbx") == 0) {

        double index = atof(properties->get("sbx.index", "20.0"));
        crossovers->add(new SBXCrossover(index));

    }

    return crossovers;
}

ArrayList<Migration> *createMigrations(Properties *properties, NetStream *migrationsStream) {

    ArrayList<Migration> *migrations = new ArrayList<Migration>(true);

    float migrationRate = (float)atof(properties->get("migration.rate", "0.01"));

    bool synchronized = false;

    if (strcmp("true", properties->get("migration.synchronized", "true")) == 0) synchronized = true;

    if (migrationsStream != NULL && migrationsStream->pnumber() > 1) {
        Migration* migration = new RingMigration(new RouletteWheelSelection(), migrationsStream);

        migration->setSynchronized(synchronized);
        migration->setMigrationRate(migrationRate);

        migrations->add(migration);
    }

    return migrations;
}


MOEA *createMOEA(Properties *properties, NetStream *migrationsStream) {

    NSGAII *moea = new NSGAII();
    int totalPopulationSize = atoi(properties->get("population.size", "20"));

    float crossoverProbability = (float)atof(properties->get("crossover.probability", "0.001"));
    int maxGenerations = atoi(properties->get("max.generations", "200"));
    int migrationGenerations = atoi(properties->get("migration.period", "5"));

    if (migrationsStream != NULL && migrationsStream->pnumber() > 0)
        moea->setCurrent(createInitialPopulation(totalPopulationSize / migrationsStream->pnumber(), properties));
    else
        moea->setCurrent(createInitialPopulation(totalPopulationSize, properties));

    moea->setCrossovers(createCrossovers(properties));
    moea->setMigrations(createMigrations(properties, migrationsStream));
    moea->setSelections(createSelections());
    moea->setMutations(createMutations(properties));

    moea->setCrossoverProbability(crossoverProbability);
    moea->setMaxGenerations(maxGenerations);
    moea->setMigrationGenerations(migrationGenerations);

    return moea;
}

Statistics *createStatistics(Properties *properties, NetStream *statisticsStream) {

    int synchronizePeriod = atoi(properties->get("update.statistics.period", "5"));

    Statistics *statistics = new Statistics(statisticsStream, createSolution(properties));

    statistics->setSynchronizePeriod(synchronizePeriod);

    return statistics;
}

Historic *createHistoric(ofstream *historicStream) {
    Historic *historic = new Historic(historicStream);
    return historic;
}

Statistics* runMOEA(Properties *properties, NetStream *statisticsStream, NetStream *migrationsStream) {
    char *fileName = new char[255];
    bzero(fileName, 255);

    Historic *historic = NULL;
    ofstream historicStream;
    if (strcmp("enable", properties->get("historic", "enable")) == 0) {
        char *directory = properties->get("historic.directory", "./");
        char *prefix = properties->get("historic.file.prefix", "population.");
        char *suffix = properties->get("historic.file.suffix", ".historic");
        sprintf(fileName, "%s%sisland.%d%s", directory, prefix, migrationsStream->my_pid(), suffix);
        historicStream.open(fileName);
        historic = createHistoric(&historicStream);
    }

    delete [] fileName;

    MOEA *moea = createMOEA(properties, migrationsStream);

    Statistics *statistics = createStatistics(properties, statisticsStream);


    moea->addObserver(statistics);
    if (historic != NULL) moea->addObserver(historic);

    (*statisticsStream) << barrier;
    (*migrationsStream) << barrier;

    moea->run();

    (*migrationsStream) << barrier;
    (*statisticsStream) << barrier;


    delete moea;

    if (historic != NULL) {
        delete historic;
    }


    return statistics;
}


StatisticsSink* runStatisticsSink(Properties *properties, NetStream *statisticsStream) {

    MOEA *moea = createMOEA(properties, NULL);
    int totalPopulationSize = atoi(properties->get("population.size", "20"));
    moea->setMaxGenerations(atoi(properties->get("integration.generations", "10")));

    StatisticsSink *statisticsSink = new StatisticsSink(createStatistics(properties, statisticsStream),
                                                        statisticsStream, moea, totalPopulationSize);

    (*statisticsStream) << barrier;

    statisticsSink->run();

    (*statisticsStream) << barrier;

    return statisticsSink;
}


void seed() {
    /*random seed*/
    /*you shall reap what you sow*/

    struct timespec time;

    clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &time);

    double seed = (double)(time.tv_nsec % 1000) / 1000.0;

    Random::getInstance()->seed(seed);

}


/** returns time in milliseconds */
long tick() {

    struct timeval start;

    gettimeofday(&start, NULL);

    return (long)(start.tv_sec*1000 + (int)start.tv_usec/1000);
}


int main(int argc, char ** argv) {

    long before = tick();

    NetStream::init(argc, argv);

    NetStream statisticsStream;
    NetStream migrationsStream;

    Properties *properties = Properties::load("moea.properties");

    int solutionSize = atoi(properties->get("solution.size", "20"));
    RealArraySolution::setSize(solutionSize);
    IntArraySolution::setSize(solutionSize);


    ArrayList<Objective> *objectives = createObjectives(properties);

    Solution::setObjectives(objectives);

    ArrayList<Restriction> *restrictions = createRestrictions(properties);
    Solution::setRestrictions(restrictions);



    if (statisticsStream.my_pid() == 0) {
        cout << "________________________________________" << endl;
        cout << "Properties" << endl;

        for (int i = 0; i < properties->length(); i++)
            cout << properties->getKey(i) << " = " << properties->getValue(i) << endl;

        cout << "________________________________________" << endl;
        cout << "processes involved " << statisticsStream.pnumber() << endl;
        if (statisticsStream.pnumber() > 1)
            cout << "islands            " << statisticsStream.pnumber() -1 << endl << endl;

    }

    seed();

    if (statisticsStream.pnumber() > 1) {
        //split in two streams one for the migrations and other for the statistics
        //the streams separations also determine two different groups of processes
        //in the "migrations group" we will find only the islands
        //in the "statistics group" we will find the islands *and* the statististics sink
        int team = 1;
        int key = statisticsStream.my_pid();
        statisticsStream << barrier;

        if (statisticsStream.my_pid() == 0) team = 2; //the process 0 will not be on the migrations group

        MPI_Comm migrationsComm = NetStream::create_group(statisticsStream.get_communicator(), team, key);

        migrationsStream.set_communicator(migrationsComm);

        if (statisticsStream.my_pid() == 0) {

            StatisticsSink *sink = runStatisticsSink(properties, &statisticsStream);
            cout << endl << endl << endl;
            cout << "_________final statistics___________________________________________" << endl;
            sink->current()->send(cout);
            cout << "____________________________________________________________________" << endl;

            long elapsed = tick() - before;
            cout << "total time in milliseconds: " << elapsed << endl;

            OctaveExport::run(sink->current()->getParetoFront(),
                                 properties->get("problem", "ZDT1"),
                                 properties->get("results.directory", "./results"),
                                 elapsed,
                                 true);

            delete sink;

        } else {

            Statistics *statistics = runMOEA(properties, &statisticsStream, &migrationsStream);
            delete statistics;

        }


    } else {

        Statistics *statistics = runMOEA(properties, &statisticsStream, &migrationsStream);

        cout << endl << endl << endl;
        cout << "_________final statistics___________________________________________" << endl;
        statistics->send(cout);
        cout << "____________________________________________________________________" << endl;

        long elapsed = tick() - before;
        cout << "total time in milliseconds: " << elapsed << endl;

        OctaveExport::run(statistics->getParetoFront(),
                             properties->get("problem", "ZDT1"),
                             properties->get("results.directory", "./results"),
                             elapsed,
                             false);

        delete statistics;
    }

    NetStream::finalize();
    Random::shutdown();
    delete properties;
    delete objectives;
    delete restrictions;
    return 0;
}

#endif
